import Vue from 'vue';
import Vuex from 'vuex';
const prototype = require('../prototype');

Vue.use(Vuex);

const vscode = acquireVsCodeApi();
const oldState = vscode.getState();
let variableIndex = 0;

function buildBuiltinNode(state, graph, node) {
  const oldNode = state.nodes.find((x) => x.id === node.id);
  switch (node.title) {
    case 'Getter': {
      const variable = graph.variables.find((x) => x.id === node.variableId);
      return {
        id: node.id,
        category: 'Builtin',
        title: 'Getter',
        color: prototype.typeColor(variable.type),
        isSelected: oldNode ? oldNode.isSelected : false,
        pos: node.pos,
        values: node.values,
        variableId: node.variableId,
        inputs: [],
        outputs: [
          {
            title: variable.title,
            type: variable.type,
            value: null,
          },
        ],
      };
    }

    case 'Setter': {
      const variable = graph.variables.find((x) => x.id === node.variableId);
      return {
        id: node.id,
        category: 'Builtin',
        title: 'Setter',
        color: prototype.typeColor(variable.type),
        isSelected: oldNode ? oldNode.isSelected : false,
        pos: node.pos,
        values: node.values,
        variableId: node.variableId,
        inputs: [
          {
            title: 'Do',
            type: 'exec',
            value: null,
          },
          {
            title: variable.title,
            type: variable.type,
            value: node.values[1],
          },
        ],
        outputs: [
          {
            title: 'Done',
            type: 'exec',
            value: null,
          },
        ],
      };
    }

    default:
      throw new Error(`Can not found builtin node: ${node.title}`);
  }
}

function buildNode(state, graph, node) {
  if (node.category === 'Builtin') {
    return buildBuiltinNode(state, graph, node);
  }

  const category = state.categories.find((x) => x.title === node.category);
  if (!category) {
    throw new Error(`Can not found category: ${category.title}`);
  }
  const prototype = category.nodes.find((x) => x.title === node.title);
  if (!prototype) {
    throw new Error(`Can not found node: ${node.title}`);
  }
  const oldNode = state.nodes.find((x) => x.id === node.id);
  return {
    id: node.id,
    category: node.category,
    title: node.title,
    color: category.color,
    isSelected: oldNode ? oldNode.isSelected : false,
    pos: node.pos,
    values: node.values,
    inputs: prototype.inputs.map((pin, index) => ({
      ...pin,
      value: node.values[index],
    })),
    outputs: prototype.outputs,
  };
}

function deleteNodes(state) {
  vscode.postMessage({
    type: 'REPLACE_GRAPH',
    prototypes: state.prototypes,
    variables: state.variables,
    nodes: state.nodes
      .filter((node) => !node.isSelected)
      .map((node) => ({
        ...node,
        color: undefined,
        isSelected: undefined,
        inputs: undefined,
        outputs: undefined,
      })),
    links: state.links.filter((link) => {
      const srcNode = state.nodes.find((x) => x.id === link.srcNodeId);
      const dstNode = state.nodes.find((x) => x.id === link.dstNodeId);
      return !srcNode.isSelected && !dstNode.isSelected;
    }),
  });
}

function initClipboard(state) {
  state.clipboard.nodes = state.nodes.filter((node) => node.isSelected);
  state.clipboard.links = state.links.filter((link) => {
    const srcNode = state.clipboard.nodes.find((node) => node.id === link.srcNodeId);
    const dstNode = state.clipboard.nodes.find((node) => node.id === link.dstNodeId);
    return srcNode && dstNode;
  });
}

const store = new Vuex.Store({
  state: {
    maxId: 0,
    origin: oldState ? oldState.origin : { x: 0, y: 0 },
    scale: oldState ? oldState.scale : 1.0,
    isDragging: false,
    isDragged: false,
    dragType: null,
    dragStartPos: { x: 0, y: 0 },
    dragEndPos: { x: 0, y: 0 },
    isMenuShow: false,
    menuPos: { x: 0, y: 0 },
    menuType: null,
    prototypes: [],
    categories: [],
    variables: [],
    nodes: [],
    links: [],
    linkingPins: [],
    clipboard: {
      nodes: [],
      links: [],
    },
  },

  mutations: {
    updateCategories(state, categories) {
      state.categories = categories;
    },

    updateGraph(state, text) {
      try {
        const graph = JSON.parse(text);
        state.maxId = 0;
        if (graph.nodes.length > 0) {
          state.maxId = Math.max(state.maxId, graph.nodes.reduce((pre, cur) => (pre.id > cur.id ? pre : cur)).id);
        }
        if (graph.variables.length > 0) {
          state.maxId = Math.max(state.maxId, graph.variables.reduce((pre, cur) => (pre.id > cur.id ? pre : cur)).id);
        }
        if (graph.prototypes.length > 0) {
          state.maxId = Math.max(state.maxId, graph.prototypes.reduce((pre, cur) => (pre.id > cur.id ? pre : cur)).id);
        }
        state.prototypes = graph.prototypes;
        state.variables = graph.variables;
        state.nodes = graph.nodes.map((node) => buildNode(state, graph, node));
        state.links = graph.links;
      } catch (err) {
        vscode.postMessage({ type: 'SHOW_ERROR', msg: err.message });
      }
    },

    setScale(state, delta) {
      state.scale = delta > 0 ? Math.max(state.scale - 0.1, 0.5) : Math.min(state.scale + 0.1, 1.0);
      vscode.setState({
        origin: state.origin,
        scale: state.scale,
      });
    },

    dragStart(state, payload) {
      const { type, x, y } = payload;
      state.isMenuShow = false;
      state.isDragging = true;
      state.isDragged = false;
      state.dragType = type;
      state.dragStartPos = { x, y };
      state.dragEndPos = { x, y };

      if (type === 'SELECTION') {
        state.nodes = state.nodes.map((node) => {
          const rect = document.getElementById(`node${node.id}`).getBoundingClientRect();
          const isSelected = x >= rect.left && x <= rect.right && y <= rect.bottom && y >= rect.top;
          return { ...node, isSelected };
        });
      } else if (type === 'NODES' && !state.nodes[payload.index].isSelected) {
        if (payload.ctrl) {
          state.nodes[payload.index].isSelected = true;
        } else {
          state.nodes.forEach((node, index) => {
            node.isSelected = index === payload.index;
          });
        }
      } else if (type === 'LINKS') {
        state.linkingPins = payload.links;
      }
    },

    dragEnd(state, payload) {
      const { x, y } = payload;
      state.isDragging = false;

      if (state.dragType === 'ORIGIN' && !state.isDragged) {
        state.isMenuShow = true;
        state.menuPos = { x, y };
        state.menuType = 'add-node-menu';
        state.linkingPins = [];
      } else if (state.dragType === 'NODES' && state.isDragged) {
        vscode.postMessage({
          type: 'MOVE_NODES',
          nodes: state.nodes
            .map((node, index) => ({ index, pos: node.pos }))
            .filter(({ index }) => state.nodes[index].isSelected),
        });
      } else if (state.dragType === 'LINKS' && state.isDragged) {
        state.isMenuShow = true;
        state.menuPos = { x, y };
        state.menuType = 'add-node-menu';
      }
    },

    drag(state, payload) {
      const { x, y } = payload;
      state.isDragged = true;
      state.dragEndPos = { x, y };
      switch (state.dragType) {
        case 'ORIGIN':
          state.origin = {
            x: state.origin.x + x - state.dragStartPos.x,
            y: state.origin.y + y - state.dragStartPos.y,
          };
          state.dragStartPos = { x, y };
          vscode.setState({
            origin: state.origin,
            scale: state.scale,
          });
          break;

        case 'SELECTION':
          const selection = {
            left: Math.min(state.dragStartPos.x, x),
            right: Math.max(state.dragStartPos.x, x),
            top: Math.min(state.dragStartPos.y, y),
            bottom: Math.max(state.dragStartPos.y, y),
          };
          state.nodes = state.nodes.map((node) => {
            const rect = document.getElementById(`node${node.id}`).getBoundingClientRect();
            const isSelected =
              selection.right >= rect.left &&
              selection.left <= rect.right &&
              selection.top <= rect.bottom &&
              selection.bottom >= rect.top;
            return { ...node, isSelected };
          });
          break;

        case 'NODES':
          state.nodes = state.nodes.map((node) => {
            if (node.isSelected) {
              return { ...node, pos: [node.pos[0] + x - state.dragStartPos.x, node.pos[1] + y - state.dragStartPos.y] };
            } else {
              return node;
            }
          });
          state.dragStartPos = { x, y };
          break;
      }
    },

    addNode(state, payload) {
      state.isMenuShow = false;

      const links = [];
      if (state.linkingPins.length > 0) {
        if (state.linkingPins[0].isInput && payload.node.outputs.length > 0) {
          state.linkingPins.forEach((link) => {
            links.push({
              index: state.links.length + links.length,
              srcNodeId: state.maxId + 1,
              srcPinTitle: payload.node.outputs[0].title,
              dstNodeId: link.nodeId,
              dstPinTitle: link.pinTitle,
            });
          });
        } else if (!state.linkingPins[0].isInput && payload.node.inputs.length > 0) {
          state.linkingPins.forEach((link) => {
            links.push({
              index: state.links.length + links.length,
              srcNodeId: link.nodeId,
              srcPinTitle: link.pinTitle,
              dstNodeId: state.maxId + 1,
              dstPinTitle: payload.node.inputs[0].title,
            });
          });
        }
      }
      vscode.postMessage({
        type: 'ADD_NODE',
        ...payload,
        id: ++state.maxId,
        index: state.nodes.length,
        pos: [state.menuPos.x - state.origin.x, state.menuPos.y - state.origin.y],
        links,
      });
    },

    deleteNodes(state) {
      deleteNodes(state);
    },

    copyNodes(state) {
      initClipboard(state);
    },

    cutNodes(state) {
      initClipboard(state);
      deleteNodes(state);
    },

    pasteNodes(state, { x, y }) {
      const offset = state.nodes.length;

      let origin = { x: Infinity, y: Infinity };
      for (const node of state.clipboard.nodes) {
        origin.x = Math.min(origin.x, node.pos[0]);
        origin.y = Math.min(origin.y, node.pos[1]);
      }

      let nodes = state.nodes;
      state.clipboard.nodes.forEach((node) => {
        nodes.push({
          ...node,
          id: ++state.maxId,
          pos: [node.pos[0] - origin.x + x - state.origin.x, node.pos[1] - origin.y + y - state.origin.y],
        });
      });

      let links = state.links;
      state.clipboard.links.forEach((link) => {
        const srcIndex = state.clipboard.nodes.findIndex((node) => node.id === link.srcNodeId) + offset;
        const dstIndex = state.clipboard.nodes.findIndex((node) => node.id === link.dstNodeId) + offset;
        links.push({
          srcNodeId: nodes[srcIndex].id,
          srcPinTitle: link.srcPinTitle,
          dstNodeId: nodes[dstIndex].id,
          dstPinTitle: link.dstPinTitle,
        });
      });

      vscode.postMessage({
        type: 'REPLACE_GRAPH',
        prototypes: state.prototypes,
        variables: state.variables,
        nodes,
        links,
      });
    },

    changePinValue(state, payload) {
      vscode.postMessage({ type: 'CHANGE_PIN_VALUE', ...payload });
    },

    addLinks(state, { isInput, nodeId, pinTitle }) {
      if (state.linkingPins.length === 0) {
        return;
      }
      if (state.linkingPins[0].isInput === isInput) {
        return;
      }
      if (state.linkingPins.find((link) => link.nodeId === nodeId)) {
        return;
      }

      const linkNode = state.nodes.find((x) => x.id === state.linkingPins[0].nodeId);
      const linkPin = state.linkingPins[0].isInput
        ? linkNode.inputs.find((x) => x.title === state.linkingPins[0].pinTitle)
        : linkNode.outputs.find((x) => x.title === state.linkingPins[0].pinTitle);

      const node = state.nodes.find((x) => x.id === nodeId);
      const pin = isInput
        ? node.inputs.find((x) => x.title === pinTitle)
        : node.outputs.find((x) => x.title === pinTitle);

      if (linkPin.type !== pin.type) {
        return;
      }

      let links = state.links;
      if (pin.type !== 'exec') {
        if (isInput) {
          links = state.links.filter((link) => {
            return !(link.dstNodeId === nodeId && link.dstPinTitle === pinTitle);
          });
        } else {
          links = state.links.filter((link) => {
            return !state.linkingPins.find((x) => {
              return x.isInput && x.nodeId === link.dstNodeId && x.pinTitle === link.dstPinTitle;
            });
          });
        }
      } else {
        if (isInput) {
          links = state.links.filter((link) => {
            return !state.linkingPins.find((x) => {
              return !x.isInput && x.nodeId === link.srcNodeId && x.pinTitle === link.srcPinTitle;
            });
          });
        } else {
          links = state.links.filter((link) => {
            return !(link.srcNodeId === nodeId && link.srcPinTitle === pinTitle);
          });
        }
      }

      state.linkingPins.forEach((link) => {
        if (isInput) {
          links.push({
            srcNodeId: link.nodeId,
            srcPinTitle: link.pinTitle,
            dstNodeId: nodeId,
            dstPinTitle: pinTitle,
          });
        } else {
          links.push({
            srcNodeId: nodeId,
            srcPinTitle: pinTitle,
            dstNodeId: link.nodeId,
            dstPinTitle: link.pinTitle,
          });
        }
      });
      state.linkingPins = [];
      vscode.postMessage({
        type: 'REPLACE_GRAPH',
        prototypes: state.prototypes,
        variables: state.variables,
        nodes: state.nodes,
        links,
      });
    },

    deleteLinks(state, payload) {
      const { isInput, nodeId, pinTitle } = payload;
      vscode.postMessage({
        type: 'REPLACE_GRAPH',
        prototypes: state.prototypes,
        variables: state.variables,
        nodes: state.nodes,
        links: state.links.filter((link) => {
          if (isInput) {
            return !(link.dstNodeId === nodeId && link.dstPinTitle === pinTitle);
          } else {
            return !(link.srcNodeId === nodeId && link.srcPinTitle === pinTitle);
          }
        }),
      });
    },

    moveLinks(state, payload) {
      const { isInput, nodeId, pinTitle, x, y } = payload;
      state.isMenuShow = false;
      state.isDragging = true;
      state.isDragged = false;
      state.dragType = 'LINKS';
      state.dragStartPos = { x, y };
      state.dragEndPos = { x, y };
      state.linkingPins = state.links
        .filter((link) => {
          if (isInput) {
            return link.dstNodeId === nodeId && link.dstPinTitle === pinTitle;
          } else {
            return link.srcNodeId === nodeId && link.srcPinTitle === pinTitle;
          }
        })
        .map((link) => {
          if (isInput) {
            return {
              isInput: false,
              nodeId: link.srcNodeId,
              pinTitle: link.srcPinTitle,
            };
          } else {
            return {
              isInput: true,
              nodeId: link.dstNodeId,
              pinTitle: link.dstPinTitle,
            };
          }
        });
    },

    addVariable(state, index) {
      vscode.postMessage({ type: 'ADD_VARIABLE', id: ++state.maxId, index });
    },

    deleteVariable(state, index) {
      vscode.postMessage({ type: 'DELETE_VARIABLE', index });
    },

    editVariableTitle(state, payload) {
      vscode.postMessage({ type: 'EDIT_VARIABLE_TITLE', ...payload });
    },

    editVariableType(state, payload) {
      vscode.postMessage({ type: 'EDIT_VARIABLE_TYPE', ...payload });
    },

    changeVariableDefault(state, payload) {
      vscode.postMessage({ type: 'CHANGE_VARIABLE_DEFAULT', ...payload });
    },

    addVariableMenu(state, payload) {
      variableIndex = payload.index;
      state.isMenuShow = true;
      state.menuPos.x = payload.x;
      state.menuPos.y = payload.y;
      state.menuType = 'add-variable-menu';
    },

    addVariableNode(state, title) {
      const variable = state.variables[variableIndex];
      state.isMenuShow = false;
      vscode.postMessage({
        type: 'ADD_VARIABLE_NODE',
        nodeIndex: state.nodes.length,
        prototypeIndex: state.prototypes.length,
        variable: {
          id: ++state.maxId,
          category: 'Builtin',
          title,
          pos: [state.menuPos.x - state.origin.x, state.menuPos.y - state.origin.y],
          values: title === 'Getter' ? [] : [null, prototype.defaultValue(variable.type)],
          variableId: variable.id,
        },
        prototype: !state.prototypes.find((x) => x.variableId === variable.id)
          ? {
              id: ++state.maxId,
              variableId: variable.id,
              title,
              inputs:
                title === 'Getter'
                  ? []
                  : [
                      {
                        title: 'Do',
                        type: 'exec',
                        key: null,
                        value: null,
                      },
                      {
                        title: variable.title,
                        type: variable.type,
                        key: variable.key,
                        value: variable.value,
                      },
                    ],
              outputs:
                title == 'Getter'
                  ? [
                      {
                        title: variable.title,
                        type: variable.type,
                        key: variable.key,
                        value: variable.value,
                      },
                    ]
                  : [
                      {
                        title: 'Done',
                        type: 'exec',
                        key: null,
                        value: null,
                      },
                    ],
            }
          : null,
      });
    },
  },

  getters: {
    isSelecting: (state) => {
      return state.isDragging && state.dragType === 'SELECTION';
    },

    isLinking: (state) => {
      return state.isDragging && state.dragType === 'LINKS';
    },

    hasLink: (state) => (isInput, nodeId, pinTitle) => {
      return (
        state.links.findIndex((x) => {
          if (isInput) {
            return x.dstNodeId === nodeId && x.dstPinTitle === pinTitle;
          } else {
            return x.srcNodeId === nodeId && x.srcPinTitle === pinTitle;
          }
        }) >= 0
      );
    },
  },
});

export default store;
